﻿using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.ServiceModel.Channels;
using System.Text;
using System.Xml;
using System.Xml.Linq;
using Lab620.WcfRadiusService.Clutch;

namespace Lab620.WcfRadiusService.Extentions
{
    static class RadiusMessageConstants
    {
        public const string RadiusEncodingName = "RadiusEncoding";
        public const string RadiusEncodingNamespace = "http://lab620.com/schemas/2009/02/netRadius";
        public const string RadiusEncodingPrefix = "Radius";
    }

    internal class RadiusMessageEncoderFactory : MessageEncoderFactory
    {
        readonly MessageEncoder encoder;

        //The Radius encoder wraps an inner encoder
        //We require a factory to be passed in that will create this inner encoder
        public RadiusMessageEncoderFactory(MessageEncoderFactory messageEncoderFactory)
        {
            if (messageEncoderFactory == null)
                throw new ArgumentNullException("messageEncoderFactory", "A valid message encoder factory must be passed to the RadiusEncoder");
            encoder = new RadiusMessageEncoder(messageEncoderFactory.Encoder);
        }

        //The service framework uses this property to obtain an encoder from this encoder factory
        public override MessageEncoder Encoder
        {
            get { return encoder; }
        }

        public override MessageVersion MessageVersion
        {
            get { return MessageVersion.Soap11; }
        }

        //This is the actual Radius encoder
        class RadiusMessageEncoder : MessageEncoder
        {
            private const string RadiusContentType = "application/x-Radius";

            //We require an inner encoder to be supplied (see comment above)
            internal RadiusMessageEncoder(MessageEncoder messageEncoder)
            {
                if (messageEncoder == null)
                    throw new ArgumentNullException("messageEncoder", "A valid message encoder must be passed to the RadiusEncoder");
            }

            #region 1
            public override string ContentType
            {
                get { return RadiusContentType; }
            }

            public override string MediaType
            {
                get { return RadiusContentType; }
            }

            public override MessageVersion MessageVersion
            {
                get { return MessageVersion.Soap12WSAddressing10; }
            }


            //One of the two main entry points into the encoder. Called by WCF to decode a buffered byte array into a Message.
            public override Message ReadMessage(ArraySegment<byte> buffer, BufferManager bufferManager, string contentType)
            {
                using (var ms = new MemoryStream(buffer.Array, buffer.Offset, buffer.Count, false))
                {
                    return ReadMessage(ms, 0, contentType);
                }
            }

            //One of the two main entry points into the encoder. Called by WCF to encode a Message into a buffered byte array.
            public override ArraySegment<byte> WriteMessage(Message message, int maxMessageSize, BufferManager bufferManager, int messageOffset)
            {
                var packet = ConvertMessageToPacket(message);
                return new ArraySegment<byte>(packet.Data, 0, packet.DataLength);
            }

            public override void WriteMessage(Message message, Stream stream)
            {
                RadiusPacket packet = ConvertMessageToPacket(message);

                stream.Write(packet.Data, 0, packet.DataLength);

                // the stream passed in, but the implementation of RadiusStream.Flush will not flush underlying
                // stream, so we need to flush here.
                stream.Flush();
            }

            #endregion

            private const string soapBodyFormat = @"<m:Authenticate xmlns:m=""{0}"" >
                  <m:userName>{1}</m:userName>
                  <m:password>{2}</m:password>
                  <m:authenticator>{3}</m:authenticator>
                </m:Authenticate>";

            public override Message ReadMessage(Stream stream, int maxSizeOfHeaders, string contentType)
            {
                var packet = new RadiusPacket(stream);
                packet.Parse();

                Debug.WriteLine("-- Incoming message: " + packet.CodeType + " : " + packet.Identifier);

                Message m = null; 
                if (packet.CodeType == RadiusCodeType.AccessRequest)
                {
                    var userNameAttrib = packet.Attributes.Find(RadiusAttributeType.UserName);
                    var passwordAttrib = packet.Attributes.Find(RadiusAttributeType.UserPassword);

                    var body = string.Format(soapBodyFormat,
                                             RadiusMessageConstants.RadiusEncodingNamespace,
                                             userNameAttrib != null ? userNameAttrib.TextValue : "",
                                             passwordAttrib != null ? passwordAttrib.Base64EncodedValue : "",
                                             Convert.ToBase64String( packet.Authenticator ));

                    using (var sr = new StringReader(body))
                    using (var reader = XmlReader.Create(sr))
                    {
                        m = Message.CreateMessage(MessageVersion,
                                                  RadiusMessageConstants.RadiusEncodingNamespace +
                                                  "/IAuthenticationService/Authenticate" , reader).CreateBufferedCopy(9999).CreateMessage();
                    }

                    m.Headers.MessageId = new UniqueId(packet.Identifier.ToString());

                    foreach (var attribute in packet.Attributes)
                    {
                        if (attribute.AttributeType.HasValue)
                        {
                            var value = attribute.TypedValue;

                            Debug.WriteLine(attribute.AttributeType.Value + ": " + value);

                            m.Headers.Add(MessageHeader.CreateHeader(attribute.AttributeType.Value.ToString(),
                                                                     RadiusMessageConstants.
                                                                         RadiusEncodingNamespace, value));
                        }
                    }

                    m.Properties.Add("IncomingPacket", packet);
               }

                return m;
            }


            private static RadiusPacket ConvertMessageToPacket(Message message)
            {
                XNamespace ns = RadiusMessageConstants.RadiusEncodingNamespace;

                var sb = new StringBuilder(4096);
                using(var sw = new StringWriter(sb))
                using(var xtw = XmlWriter.Create(sw))
                    message.WriteBody(xtw);

                XElement body;
                var msgBodyStr = sb.ToString();

                Debug.WriteLine("-- Outgoing message: " + message.Headers.RelatesTo);
                Debug.WriteLine(msgBodyStr);

                using(var sr = new StringReader(msgBodyStr))
                    body = XElement.Load(sr);

                var incomingPacket = (RadiusPacket)message.Properties["IncomingPacket"];
                var sharedSecret = (byte[])message.Properties["SharedSecret"];

                var packet = new RadiusPacket
                                 {
                                     CodeType = RadiusCodeType.AccessReject,
                                     Identifier = byte.Parse(message.Headers.RelatesTo.ToString()),
                                     Authenticator = incomingPacket.Authenticator
                                 };

                var outgoingProperties = message.Properties["PacketAttributes"] as IDictionary<RadiusAttributeType, object>;
                if (outgoingProperties != null)
                {
                    foreach (var pair in outgoingProperties)
                    {
                        packet.Attributes.Add(new RadiusAttribute(pair.Key, (string)pair.Value));
                    }
                }

                if (message.IsFault)
                {
                    var radiusFault = body.DescendantsAndSelf(ns + "RadiusFault").FirstOrDefault();
                    if (radiusFault != null)
                    {
                        var messageElement = radiusFault.Element(ns + "Message");
                        if (messageElement != null)
                        {
                            var faultMessage = messageElement.Value;
                            packet.Attributes.Add(new RadiusAttribute(RadiusAttributeType.ReplyMessage, faultMessage));
                        }
                    }
                }
                else
                {
                    var res = body.DescendantsAndSelf(ns + "AuthenticateResult").FirstOrDefault();
                    if (res != null)
                    {
                        if (bool.Parse(res.Value))
                        {
                            packet.CodeType = RadiusCodeType.AccessAccept;
                        }
                    }
                }

                packet.Serialize(sharedSecret);
                return packet;
            }
        }
    }
}
